<?php

declare(strict_types=1);
/**
 * This file is part of Hyperf.
 *
 * @link     https://www.hyperf.io
 * @document https://hyperf.wiki
 * @contact  group@hyperf.io
 * @license  https://github.com/hyperf/hyperf/blob/master/LICENSE
 */

namespace App\Websocket;

use App\Logic\ImLogic;
use App\Logic\LiveLogic;
use App\Logic\UserLogic;
use App\Model\ImGroupMessage;
use App\Model\ImMessage;
use App\Model\ImSession;
use App\Model\Live;
use App\Model\LiveGift;
use App\Model\Play;
use App\Model\User;
use App\Model\UserModelAssist;
use App\Model\UserTask;
use App\Model\UserToken;
use App\Service\PushService;
use App\Service\SettingService;
use App\Utils\Enum\AccountEnum;
use App\Utils\Enum\ClientEnum;
use App\Utils\Enum\ImEnum;
use App\Utils\Enum\UserEnum;
use App\Utils\Im;
use Exception;
use Hyperf\Contract\OnCloseInterface;
use Hyperf\Contract\OnMessageInterface;
use Hyperf\Contract\OnOpenInterface;
use Hyperf\Engine\WebSocket\Frame;
use Hyperf\Engine\WebSocket\Response;
use Hyperf\Logger\LoggerFactory;
use Hyperf\Redis\Redis;
use Hyperf\WebSocketServer\Constant\Opcode;
use Hyperf\WebSocketServer\Context;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use RedisException;
use Swoole\WebSocket\Server as WebSocketServer;

class ImWebsocket implements OnMessageInterface, OnOpenInterface, OnCloseInterface
{
    public const int HEARTBEAT_INTERVAL = 10;
    public const string KEY_FDS = 'im:fds'; //sorted set
    public const string KEY_FD_INFO = 'fd:'; //sorted set
    public const string KEY_USER = 'im:user:'; //string
    public const string KEY_GROUP = 'group:'; //set

    protected LoggerInterface $logger;

    protected ContainerInterface $container;

    protected Redis $redis;

    protected string $heartbeatMsg;

    protected array $taskSetting = [];

    protected $pushService;

    protected $chatRoomMsgCount = [];

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        try {
            $this->logger = $this->container->get(LoggerFactory::class)->get('live-im', 'default');
            $this->redis = $this->container->get(Redis::class);
            $this->pushService = $this->container->get(PushService::class);

            $this->heartbeatMsg = Im::Create('ImHeartbeatCmd');
            $this->initTaskSetting();

        } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
        }
    }

    /**
     * 初始化任务配置
     * @return void
     */
    private function initTaskSetting(): void
    {
        $setting = SettingService::getGroupSetting('task');
        [$chatReward, $maxRewardTimes] = explode('|', $setting['task_chat_reward']);
        $this->taskSetting['chat'] = ['reward' => $chatReward, 'maxRewardTimes' => $maxRewardTimes];
        [$watchReward, $onceMinutes, $maxRewardTimes] = explode('|', $setting['task_watch_live_reward']);
        $this->taskSetting['watch'] = ['reward' => $watchReward, 'onceMinutes' => $onceMinutes, 'maxRewardTimes' => $maxRewardTimes];
    }

    /**
     * 消息回调
     * @param $server
     * @param $frame
     * @return void
     */
    public function onMessage($server, $frame): void
    {
        if ($frame->opcode == Opcode::PING) {
            $response = (new Response($server))->init($frame);
            $response->push(new Frame(opcode: Opcode::PONG));
            return;
        }

        try {
            $msg = Im::Parse($frame);
            if ($msg) {
                $this->logger->info(sprintf('收到%s消息 %s', $msg['msgType'], new_json_encode($msg['msgBody'])));
                $action = '_event' . ucfirst($msg['msgType']);
                if (method_exists($this, $action)) {
                    echo sprintf('fd:%d msg:%s', $frame->fd, $msg['msgType']) . "\n";
                    call_user_func_array([$this, $action], [$server, $frame->fd, $msg['msgBody']]);
                } else {
                    $this->logger->warning("未知消息类型:{$msg['msgType']}");
                }
            } else {
                $this->logger->warning('消息解析失败');
            }
        } catch (Exception $e) {
            echo $e->getFile() . "\n" . $e->getLine() . "\n";
            $this->logger->warning('消息异常:' . $e->getMessage());
        }
    }

    /**
     * 心跳消息
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImHeartbeatCmd(WebSocketServer $serv, $fd, $data): void
    {
        try {
            $this->redis->zAdd(self::KeyFds(), time(), (string)$fd);
        } catch (RedisException $e) {
            $this->logger->warning(sprintf('redis操作异常:%s', $e->getMessage()));
        }
        $serv->push($fd, $this->heartbeatMsg, WEBSOCKET_OPCODE_BINARY);


        //任务
        $userInfo = Context::get('userInfo');
        $groupId = Context::get('groupId', 0);
        $watchStartTime = Context::get('watchStartTime', 0);
        if ($userInfo['roleId'] !== UserEnum::USER_TYPE_VISITOR && $groupId && $watchStartTime && time() - $watchStartTime >= 60) {
            Context::set('watchStartTime', time());
            $task = UserLogic::getUserTask($userInfo['id']);
            $task['watch_live_minutes']++;
            UserTask::query()->where('id', $task['id'])->increment('watch_live_minutes');
            if ($task['watch_live_minutes'] % $this->taskSetting['watch']['onceMinutes'] === 0 && $task['watch_live_minutes'] <= $this->taskSetting['watch']['onceMinutes'] * $this->taskSetting['watch']['maxRewardTimes']) {
                UserLogic::incrCoins($userInfo['id'], $this->taskSetting['watch']['reward'], AccountEnum::TYPE_TASK_REWARD, "观看直播{$task['watch_live_minutes']}分钟任务奖励");
                $data = [
                    'taskType' => UserEnum::TASK_WATCH_LIVE,
                    'taskName' => '观看直播' . $task['watch_live_minutes'] . '分钟',
                    'taskReward' => '金币 +' . $this->taskSetting['watch']['reward'],
                ];
                $serv->push($fd, Im::Create('ImDoResult', ['code' => ImEnum::IM_DO_TASK_RESULT, 'data' => new_json_encode($data)]), WEBSOCKET_OPCODE_BINARY);
            }
        }
    }

    /**
     * 登录命令
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImLoginCmd(WebSocketServer $serv, $fd, $data): void
    {
        Context::destroy('userId');
        Context::destroy('userInfo');
        $userInfo = $this->_parseUserInfo($data);
        $ret = [
            'code' => $userInfo ? 1 : 0,
            'msg' => $userInfo ? 'succ' : 'fai',
            'unreadMsgNum' => 0,
            'newUserApply' => 0
        ];

        if ($userInfo) {
            $userInfo['roleId'] = $userInfo['role_id'];
            unset($userInfo['role_id']);
            if (isset($userInfo['token'])) {
                $ret['token'] = $userInfo['token'];
                unset($userInfo['token']);
                $ret['userInfo'] = new_json_encode($userInfo);
            }
            $userInfo['level'] = UserLogic::getUserLevelInfo($userInfo['integral'])['levelNum'];
            $res = $this->bindUser($fd, $userInfo['id']);
            Context::set('userInfo', $userInfo);
            $ret['unreadMsgNum'] = ImLogic::getUnreadMsgNum($userInfo['id']);
            $ret['newUserApply'] = ImLogic::getUnDoApplyNum($userInfo['id']);
        }
        $serv->push($fd, Im::Create('ImLoginResult', $ret), WEBSOCKET_OPCODE_BINARY);
    }

    /**
     * 加入聊天室命令
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImChatJoinCmd(WebSocketServer $serv, $fd, $data): void
    {
        $userInfo = Context::get('userInfo');
        if (!$userInfo || empty($data['roomId']) && empty($data['matchId']) || !empty($data['roomId']) && !Live::query()->where('room_id', $data['roomId'])->exists() || !empty($data['matchId']) && !Play::query()->where('match_id', $data['matchId'])->exists()) {
            $serv->push($fd, Im::Create('ImChatJoinResult', ['code' => 0, 'msg' => '参数错误']), WEBSOCKET_OPCODE_BINARY);
        } else {
            $groupId = empty($data['roomId']) ? $data['matchId'] : $data['roomId'];
            Context::set('groupId', $groupId);
            $this->redis->sAdd(self::KeyGroup(empty($data['roomId']) ? $data['matchId'] : $data['roomId']), $fd);
            $packMessageList = $packSessionList = [];
            $messageList = ImLogic::getGroupLastMsg($groupId, 30);
            foreach ($messageList as $r) {
                array_unshift($packMessageList, Im::Pack('ImNewChatMsgResult', $r));
            }
            if (!empty($data['roomId'])) {
                unset($messageList);
                $this->_sendAssistantAutoMessage($data['roomId'], $userInfo['id']);
                $sessionList = ImLogic::getConversation($userInfo['id'], false);
                foreach ($sessionList as $s) {
                    $packSessionList[] = Im::Pack('ImSessionResult', [
                        'sessionId' => $s['sessionId'],
                        'userId' => $s['userInfo']['userId'],
                        'userType' => $s['userInfo']['userRoleId'],
                        'userAvatar' => $s['userInfo']['userAvatar'],
                        'userNick' => $s['userInfo']['userNick'],
                        'unreadNum' => $s['unReadNum'],
                        'lastMsgBrief' => $s['lastMsg'] ? $s['lastMsg']['brief'] : '',
                        'lastMsgRead' => $s['lastMsg'] ? $s['lastMsg']['hasRead'] : 0,
                        'lastMsgTime' => $s['lastMsg'] ? $s['lastMsg']['time'] : 0,
                    ]);
                }
                unset($sessionList);
            }
            $serv->push($fd, Im::Create('ImChatJoinResult', ['code' => 1, 'msg' => 'succ', 'unreadLetterNum' => 0, 'sessionList' => $packSessionList, 'messageList' => $packMessageList]), WEBSOCKET_OPCODE_BINARY);
            $packMsg = Im::Create('ImNewChatMsgResult', [
                'msgId' => random(10),
                'userId' => $userInfo['id'],
                'userNick' => $userInfo['nickname'],
                'userType' => $userInfo['roleId'],
                'userLevel' => $userInfo['level'],
                'msgType' => ImEnum::MSG_TYPE_WELCOME,
                'msgData' => '',
                'msgTime' => time(),
            ]);
            $this->_sendGroupMsg($serv, $groupId, $packMsg);

            //观看任务
            Context::set('watchStartTime', time());
        }
    }

    /**
     * 退出房间
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImChatQuitCmd(WebSocketServer $serv, $fd, $data): void
    {
        Context::destroy('watchStartTime');
        $groupId = Context::get('groupId', 0);
        if ($groupId) {
            try {
                $this->redis->sRem(self::KeyGroup($groupId), $fd);
            } catch (RedisException $e) {
            }
        }
        $serv->push($fd, Im::Create('ImChatQuitResult', [
            'code' => 1,
            'msg' => 'succ'
        ]), WEBSOCKET_OPCODE_BINARY);
    }

    /**
     * 发送聊天室消息
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return bool|void
     */
    private function _eventImSendChatMsgCmd(WebSocketServer $serv, $fd, $data)
    {
        $groupId = Context::get('groupId', 0);
        $userInfo = Context::get('userInfo');
        if (!$groupId) {
            return $serv->push($fd, Im::Create('ImSendChatMsgResult', [
                'code' => 0,
                'msg' => '请重新进入房间',
                'msgId' => $data['msgId']
            ]), WEBSOCKET_OPCODE_BINARY);
        }
        if (empty($data['msgId']) || empty($data['msgType']) || empty($data['msgData'])) {
            return $serv->push($fd, Im::Create('ImSendChatMsgResult', [
                'code' => 0,
                'msg' => '参数错误',
                'msgId' => $data['msgId']
            ]), WEBSOCKET_OPCODE_BINARY);
        }
        $msg = ImGroupMessage::create([
            'msg_id' => $data['msgId'],
            'group_id' => $groupId,
            'userid' => $userInfo['id'],
            'user_nick' => $userInfo['nickname'],
            'user_type' => $userInfo['roleId'],
            'user_level' => $userInfo['level'],
            'msg_type' => $data['msgType'],
            'msg_data' => $data['msgData'],
            'send_time' => time()
        ]);
        $serv->push($fd, Im::Create('ImSendChatMsgResult', [
            'code' => 1,
            'msg' => 'succ',
            'msgId' => $data['msgId']
        ]), WEBSOCKET_OPCODE_BINARY);
        $packMsg = Im::Create('ImNewChatMsgResult', [
            'msgId' => $msg['msg_id'],
            'userId' => $msg['userid'],
            'userNick' => $msg['user_nick'],
            'userType' => $msg['user_type'],
            'userLevel' => $msg['user_level'],
            'msgType' => $msg['msg_type'],
            'msgData' => $msg['msg_data'],
            'msgTime' => $msg['send_time'],
        ]);
        $this->_sendGroupMsg($serv, $groupId, $packMsg);

        if (isset($this->chatRoomMsgCount[$groupId])) {
            $this->chatRoomMsgCount[$groupId]++;
        } else {
            $this->chatRoomMsgCount[$groupId] = 1;
        }
        if (is_numeric($groupId)) {
            $this->_autoLiveAnnouncement($serv, $groupId);
        }

        //任务
        if ($userInfo['roleId'] != UserEnum::USER_TYPE_VISITOR) {
            $task = UserLogic::getUserTask($userInfo['id']);
            $task['chat_times']++;
            UserTask::query()->where('id', $task['id'])->increment('chat_times');
            if ($task['chat_times'] <= $this->taskSetting['chat']['maxRewardTimes']) {
                UserLogic::incrIntegral($userInfo['id'], $this->taskSetting['chat']['reward'], AccountEnum::TYPE_TASK_REWARD, "聊天发言第{$task['chat_times']}次任务奖励");
                $data = [
                    'taskType' => UserEnum::TASK_WATCH_LIVE,
                    'taskName' => '聊天发言',
                    'taskReward' => '金币 +' . $this->taskSetting['chat']['reward'],
                ];
                $serv->push($fd, Im::Create('ImDoResult', ['code' => ImEnum::IM_DO_TASK_RESULT, 'data' => new_json_encode($data)]), WEBSOCKET_OPCODE_BINARY);
            }
        }
    }

    /**
     * 操作
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImDoCmd(WebSocketServer $serv, $fd, $data): void
    {
        $userInfo = Context::get('userInfo');
        $groupId = Context::get('groupId', 0);

        //重载会话
        if ($data['code'] == ImEnum::IM_DO_RELOAD_SESSION_LIST) {
            $packSessionList = [];
            $sessionList = ImLogic::getConversation($userInfo['id'], false);
            foreach ($sessionList as $s) {
                $packSessionList[] = [
                    'sessionId' => $s['sessionId'],
                    'userId' => $s['userInfo']['userId'],
                    'userType' => $s['userInfo']['userRoleId'],
                    'userAvatar' => $s['userInfo']['userAvatar'],
                    'userNick' => $s['userInfo']['userNick'],
                    'unreadNum' => $s['unReadNum'],
                    'lastMsgBrief' => $s['lastMsg'] ? $s['lastMsg']['brief'] : '',
                    'lastMsgRead' => $s['lastMsg'] ? $s['lastMsg']['hasRead'] : 0,
                    'lastMsgTime' => $s['lastMsg'] ? $s['lastMsg']['time'] : 0,
                ];
            }
            $serv->push($fd, Im::Create('ImDoResult', ['code' => $data['code'], 'data' => new_json_encode($packSessionList)]), WEBSOCKET_OPCODE_BINARY);
        } //消息已读
        elseif ($data['code'] == ImEnum::IM_DO_READ_MSG) {
            $msgIds = array_unique(array_filter(explode(',', trim($data['data']))));
            if (!empty($msgIds)) {
                ImMessage::query()->whereIn('msg_id', $msgIds)->where('to_userid', $userInfo['id'])->update(['has_read' => 1, 'read_time' => time()]);
                $targetUserId = ImMessage::query()->where('msg_id', $msgIds[0])->value('from_userid');
                if ($targetUserId) {
                    $targetFd = $this->getFdByUid($targetUserId);
                    if ($targetFd) {
                        $serv->push($targetFd, Im::Create('ImDoResult', ['code' => $data['code'], 'data' => $data['data']]), WEBSOCKET_OPCODE_BINARY);
                    }
                }
                $sessionId = ImLogic::createSessionId($userInfo['id'], $targetUserId);
                $sessionInfo = ImSession::query()->where(['session_id' => $sessionId, 'userid' => $userInfo['id']])->first();
                if ($sessionInfo) {
                    $sessionUpdate = [
                        'unread' => ImMessage::query()->where(['session_id' => $sessionId, 'to_userid' => $userInfo['id'], 'has_read' => 0])->count()
                    ];
                    if (in_array($sessionInfo['last_msg_id'], $msgIds)) {
                        $sessionUpdate['last_msg_read'] = 1;
                    }
                    ImSession::query()->where('id', $sessionInfo['id'])->update($sessionUpdate);
                }
                $this->_eventImDoCmd($serv, $fd, ['code' => ImEnum::IM_DO_RELOAD_SESSION_LIST]);
            }
        } //赠送礼物
        elseif ($data['code'] == ImEnum::IM_DO_SEND_GIFT) {
            $param = new_json_decode($data['data']);
            $result = ['status' => true, 'msg' => ''];
            if ($userInfo && isset($param['roomId'])) {
                try {
                    $gift = LiveGift::query()->where('id', $param['giftId'])->first();
                    LiveLogic::sendGift($userInfo['id'], $param['roomId'], $param['giftId'], $param['quantity']);
                } catch (Exception $e) {
                    $result['status'] = false;
                    $result['msg'] = $e->getMessage();
                }
            } else {
                $result['status'] = false;
                $result['msg'] = '参数错误';
            }
            $serv->push($fd, Im::Create('ImDoResult', ['code' => $data['code'], 'data' => new_json_encode($result)]), WEBSOCKET_OPCODE_BINARY);
            if ($result['status']) {
                $msg = ImGroupMessage::create([
                    'msg_id' => create_randomstr(10),
                    'group_id' => $groupId,
                    'userid' => $userInfo['id'],
                    'user_nick' => $userInfo['nickname'],
                    'user_type' => $userInfo['roleId'],
                    'user_level' => $userInfo['level'],
                    'msg_type' => ImEnum::MSG_TYPE_GIFT,
                    'msg_data' => new_json_encode([
                        'name' => $gift['name'],
                        'tag' => $gift['tag'],
                        'quantity' => $param['quantity']
                    ]),
                    'send_time' => time()
                ]);
                $packMsg = Im::Create('ImNewChatMsgResult', [
                    'msgId' => $msg['msg_id'],
                    'userId' => $msg['userid'],
                    'userNick' => $msg['user_nick'],
                    'userType' => $msg['user_type'],
                    'userLevel' => $msg['user_level'],
                    'msgType' => $msg['msg_type'],
                    'msgData' => $msg['msg_data'],
                    'msgTime' => $msg['send_time'],
                ]);
                $this->_sendGroupMsg($serv, $groupId, $packMsg);
            }
        }
    }

    /**
     * 发送私聊消息
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImSendMsgCmd(WebSocketServer $serv, $fd, $data): void
    {
        $userId = Context::get('userId', 0);
        $userInfo = Context::get('userInfo');
        if (!$userId) {
            $ret = ['code' => 2, 'msg' => '请登录'];
        } elseif (empty($data['msgId']) || !intval($data['toUserId']) || !intval($data['msgType']) || empty($data['msgData'])) {
            $ret = ['code' => 3, 'msg' => '参数缺失'];
        } elseif (!in_array($data['msgType'], ImEnum::allowSendMsgTypes())) {
            $ret = ['code' => 4, 'msg' => '该消息类型不被允许'];
        } else {
            if (!ImLogic::isAssistChat($userId, $data['toUserId']) && !ImLogic::isFriend($userId, $data['toUserId'])) {
                $ret = ['code' => 5, 'msg' => 'TA还不是你的好友，不能发送信息'];
                goto RESPONSE;
            }
            //TODO 校验消息格式
            //TODO 消息敏感词处理
            //TODO 消息重复检测 redis比较上一条消息
            try {
                $msg = ImLogic::createMessage($userId, $data['toUserId'], $data['msgType'], $data['msgData'], $data['msgId']);
                $ret = ['code' => 1, 'msg' => 'succ'];
            } catch (Exception $e) {
                $ret = ['code' => 0, 'msg' => '发送失败:' . $e->getMessage()];
            }
        }
        RESPONSE:
        $ret['msgId'] = $data['msgId'];
        $serv->push($fd, Im::Create('ImSendMsgResult', $ret), WEBSOCKET_OPCODE_BINARY);

        if ($ret['code'] == 1) {
            $targetUserFd = $this->getFdByUid($data['toUserId']);
            $msgResult = [
                'msgId' => $msg['msg_id'],
                'userId' => $userId,
                'msgType' => $msg['msg_type'],
                'msgData' => $msg['msg_data'],
                'msgTime' => $msg['send_time']
            ];
            if ($targetUserFd) {
                $serv->push($targetUserFd, Im::Create('ImNewMsgResult', $msgResult), WEBSOCKET_OPCODE_BINARY);
            }

            $remarkNickName = ImLogic::getFriendRemarkNickName($data['toUserId'], $userId);
            $this->pushService->push($data['toUserId'], [
                'title' => '您有一条新消息',
                'content' => '你收到了1条新消息',
                'data' => [
                    'type' => 'IM_MSG',
                    'data' => array_merge($msgResult, [
                        'userAvatar' => img_url($userInfo['avatar'], 80),
                        'userNick' => empty($remarkNickName) ? $userInfo['nickname'] : $remarkNickName,
                    ])
                ]
            ]);
        }
    }

    /**
     * 退出
     * @param WebSocketServer $serv
     * @param $fd
     * @param $data
     * @return void
     */
    private function _eventImQuitCmd(WebSocketServer $serv, $fd, $data): void
    {
        try {
            $userid = Context::get('userId', 0);
            if ($userid) {
                $this->redis->del(self::KeyUser($userid));
            }
            $this->redis->del(self::KeyFd($fd));
            $this->redis->zRem(self::KeyFds(), $fd);
            Context::destroy('userId');
            Context::destroy('groupId');
            Context::destroy('userInfo');
            $serv->push($fd, Im::Create('ImQuitResult', ['code' => 1]), WEBSOCKET_OPCODE_BINARY);
        } catch (RedisException $e) {
            //TODO 记录日志
        }
    }

    /**
     * 发送群聊消息
     * @param WebSocketServer $serv
     * @param $chatGroupId
     * @param $msg
     * @return void
     */
    private function _sendGroupMsg(WebSocketServer $serv, $chatGroupId, $msg): void
    {
        try {
            $fds = $this->redis->sMembers(self::KeyGroup($chatGroupId));
            if (!empty($fds)) {
                foreach ($fds as $fd) {
                    $fd = (int)$fd;
                    try {
                        if ($serv->isEstablished($fd)) {
                            $serv->push($fd, $msg, WEBSOCKET_OPCODE_BINARY);
                        } else {
                            $serv->disconnect($fd);
                            $this->logger->warning(sprintf('连接丢失 主动关闭 fd=%d', $fd));
                        }
                    } catch (Exception $e) {
                        $this->logger->warning(sprintf('连接推送或关闭异常 fd=%d msg=%s', $fd, $e->getMessage()));
                    }
                }
            }
        } catch (RedisException $e) {
            $this->logger->warning(sprintf('Redis获取房间成员fd异常 msg=%s', $e->getMessage()));
        }
    }

    /**
     * 自动直播公告
     * @param $serv
     * @param $roomId
     * @return void
     */
    private function _autoLiveAnnouncement($serv, $roomId): void
    {
        $userInfo = Context::get('userInfo');
        $autoAnnouncementMsgNum = SettingService::get('live_auto_announcement', 0);
        if ($autoAnnouncementMsgNum <= 1 || $this->chatRoomMsgCount[$roomId] % $autoAnnouncementMsgNum !== 0) {
            return;
        }

        $info = Live::query()->where('room_id', $roomId)->with(['user'])->first();
        if (!$info || empty($info['user']) || empty($info['announcement'])) {
            return;
        }

        $notice = [
            'title' => $info['user']['nickname'],
            'content' => $info['announcement']
        ];
        $msg = ImGroupMessage::create([
            'msg_id' => create_randomstr(10),
            'group_id' => $roomId,
            'userid' => $userInfo['id'],
            'user_nick' => $userInfo['nickname'],
            'user_type' => $userInfo['roleId'],
            'user_level' => $userInfo['level'],
            'msg_type' => ImEnum::MSG_TYPE_LIVE_NOTICE,
            'msg_data' => new_json_encode($notice),
            'send_time' => time()
        ]);
        $packMsg = Im::Create('ImNewChatMsgResult', [
            'msgId' => $msg['msg_id'],
            'userId' => $msg['userid'],
            'userNick' => $msg['user_nick'],
            'userType' => $msg['user_type'],
            'userLevel' => $msg['user_level'],
            'msgType' => $msg['msg_type'],
            'msgData' => $msg['msg_data'],
            'msgTime' => $msg['send_time'],
        ]);
        $this->_sendGroupMsg($serv, $roomId, $packMsg);
    }

    /**
     * 自动发送助理消息
     * @param $roomId
     * @param $userId
     * @return void
     */
    private function _sendAssistantAutoMessage($roomId, $userId): void
    {
        $assistantUserId = Live::query()->where('room_id', $roomId)->value('assistant_userid');
        if ($assistantUserId) {
            $assistInfo = UserModelAssist::query()->where('userid', $assistantUserId)->first();
            $sessionId = ImLogic::createSessionId($assistantUserId, $userId);
            if (!empty($assistInfo['auto_message']) && !ImSession::query()->where(['session_id' => $sessionId])->exists()) {
                try {
                    $msg = ImLogic::createMessage($assistantUserId, $userId, ImEnum::MSG_TYPE_TEXT, $assistInfo['auto_message'], random(10), true);
                } catch (Exception $e) {
                }
            }
        }
    }

    /**
     * 设置FD信息
     * @param $fd
     * @param $var1
     * @param $var2
     * @return bool
     */
    private function setFdInfo($fd, $var1, $var2): bool
    {
        try {
            if (is_array($var1)) {
                $this->redis->hMSet(self::KeyFd($fd), $var1);
            } else {
                $this->redis->hSet(self::KeyFd($fd), $var1, $var2);
            }
            return true;
        } catch (RedisException $e) {
            return false;
        }
    }

    public static function KeyFd($fd): string
    {
        return self::KEY_FD_INFO . $fd;
    }

    public static function KeyFds(): string
    {
        return self::KEY_FDS;
    }

    /**
     * KEY-用户
     * @param $uid
     * @return string
     */
    public static function KeyUser($uid): string
    {
        return self::KEY_USER . $uid;
    }

    public static function KeyGroup($groupId): string
    {
        return self::KEY_GROUP . $groupId;
    }

    /**
     * 获取用户FD
     * @param $userid
     * @return int
     */
    public function getFdByUid($userid): int
    {
        try {
            $fd = $this->redis->get(self::KeyUser($userid));
            return empty($fd) ? 0 : (int)$fd;
        } catch (RedisException $e) {
            return 0;
        }
    }

    /**
     * 绑定用户
     * @param $fd
     * @param $userid
     * @return bool
     */
    private function bindUser($fd, $userid): bool
    {
        Context::set('userId', $userid);
        try {
            $this->redis->set(self::KeyUser($userid), $fd);
            return true;
        } catch (RedisException $e) {
            echo $e->getMessage();
            return false;
        }
    }

    /**
     * 解析用户信息
     * @param $data
     * @return array|false
     */
    private function _parseUserInfo($data): array|false
    {
        $fields = ['id', 'username', 'nickname', 'mobile', 'email', 'role_id', 'avatar', 'integral'];
        if (empty($data['token']) && empty($data['deviceId'])) {
            return false;
        }
        if (!empty($data['token'])) {
            $userInfo = UserToken::query()->where('token', $data['token'])->with(['user' => function ($q) use ($fields) {
                $q->select($fields);
            }])->first();
            if (!$userInfo) {
                return false;
            }
            return $userInfo['user']->toArray();
        } else {
            $visitUserInfo = User::query()->where(['role_id' => UserEnum::USER_TYPE_VISITOR, 'device_id' => $data['deviceId']])->select($fields)->first();
            if ($visitUserInfo) {
                $ret = $visitUserInfo->toArray();
                $ret['token'] = UserLogic::createToken($ret, ClientEnum::H5);
                return $ret;
            } else {
                $rand = random(10);
                try {
                    $user = UserLogic::register([
                        'username' => 'U' . $rand,
                        'nickname' => '游客' . $rand,
                        'roleId' => UserEnum::USER_TYPE_VISITOR,
                        'deviceId' => $data['deviceId'],
                        'password' => md5(create_randomstr(10)),
                        'client' => 0,
                        'ip' => '0.0.0.0'
                    ]);
                    $ret = [];
                    foreach ($fields as $field) {
                        $ret[$field] = $user[$field];
                    }
                    $token = UserLogic::createToken($user->toArray(), ClientEnum::H5);
                    $ret['token'] = $token;
                    return $ret;
                } catch (Exception $e) {
                    return false;
                }
            }
        }
    }

    public function onClose($server, int $fd, int $reactorId): void
    {
        try {
            $this->redis->del(self::KeyFd($fd));
            $this->redis->zRem(self::KeyFds(), (string)$fd);
            $userInfo = Context::get('userInfo');
            if ($userInfo) {
                $this->redis->del(self::KeyUser($userInfo['id']));
            }
            $groupId = Context::get('groupId', 0);
            if ($groupId) {
                $this->redis->sRem(self::KeyGroup($groupId), $fd);
            }
            $this->logger->info(sprintf('连接ID=%d 用户ID=%d 断开', $fd, $userInfo['id'] ?? 0));
        } catch (RedisException $e) {
            $this->logger->warning(sprintf('redis操作异常:%s', $e->getMessage()));
        }
    }

    public function onOpen($server, $request): void
    {
        try {
            $this->redis->zAdd(self::KEY_FDS, time(), (string)$request->fd);
            $this->redis->hSet(self::KeyFd($request->fd), 'openTime', time());
            $this->logger->info(sprintf('连接%d 连接成功', $request->fd));
        } catch (RedisException $e) {
            $this->logger->warning(sprintf('redis操作异常:%s', $e->getMessage()));
        }
    }
}
